Minimal, generic case class `copy` function in Scala 3
NickName:mikołak Ask DateTime:2022-04-10T06:46:14

Minimal, generic case class `copy` function in Scala 3

In Scala (both 2 and 3), the copy method of case classes is synthetic, and so cannot be generalized using simple type clauses. In Scala 2, this led to generically processing case classes being done via shapeless. In Scala 3, however, support for abstracting over product types is substantially improved, alongside with other new language features, which leads to the following question:


In Scala 3, how can one create a generic copy function for all cases classes, while minimizing implementation complexity, usage complexity, and runtime resource cost?

The following, in particular, should be avoided:

  • explicit, instantiated "witness" objects for type-recursive processing, like in this example;
  • any need to explicitly define generic types for the "genericy copy" function call: ideally, all generics and givens should be infer-able;
  • use of any external libraries, unless they fulfill the criteria defined here.

What has been tried:

Let's suppose we have the following two test case classes:

case class CC1(a: String)

case class CC2(a: String, b: Int)

A naive first attempt would be this:

def genCopyV1[CC <: Product](
    cc: CC,
    copy: ccMirror.MirroredElemTypes => ccMirror.MirroredElemTypes
)(using
    ccMirror: ProductOf[CC]
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))

This fails with two Not found: ccMirror errors, since Scala 3 only supports dependent types in return values.

We can maybe try to improve it with some path-dependent types:

def genCopyV2[CC <: Product](
    cc: CC,
    copy: ProductOf[CC]#MirroredElemTypes => ProductOf[CC]#MirroredElemTypes
)(using
    ccMirror: ProductOf[CC]
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))

This actually compiles, but only until trying out actual usage:

println(generic.genCopyV2(CC1("a"), _ => Tuple1("b")))
//Found:    Tuple1[String]
//Required: deriving.Mirror.ProductOf[CC1]#MirroredElemTypes

println(generic.genCopyV2(CC2("a", 1), (a, b) => (a + "b", b + 1)))
// Wrong number of parameters, expected: 1

the errors are likely caused by the ProductOf[CC]#MirroredElemTypes not actually being bound to the particular Mirror instance resolved for ProductOf[CC].

Finally, if we bind the tuple representation type* :

def genCopyV3[CC <: Product, TupleType <: Tuple](cc: CC, copy: TupleType => TupleType)(using
    ccMirror: ProductOf[CC],
    tupleEqEnv: TupleType =:= ccMirror.MirroredElemTypes
): CC =
  ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[TupleType]))

we can finally use the function:

println(generic.genCopyV3[CC1, String *: EmptyTuple](CC1("a"), _ => Tuple1("b")))
// CC1(b)

println(generic.genCopyV3[CC2, (String, Int)](CC2("a", 1), (a, b) => (a + "b", b + 1)))
// CC2(ab,2)

println(generic.genCopyV3(CC2("a", 1), (a: String, b: Int) => (a + "b", b + 1)))
// CC2(ab,2)

Of course, this is not very useful, since the calling code needs to either explicitly specify the generic parameters, or bind them some other way (like in the argument function signature of the final usage example).


* for simplicity of use (not needing Tuple1), we can also add the 1-element case class special case:

def genCopyV3[CC <: Product, Single](cc: CC, copy: Single => Single)(using
    ccMirror: ProductOf[CC],
    tupleEqEnv: Single *: EmptyTuple =:= ccMirror.MirroredElemTypes,
    singleNotTupleEnv: NotGiven[Single =:= Tuple]
): CC =
  ccMirror.fromProduct(Tuple(copy(cc.productElement(0).asInstanceOf[Single])))

Copyright Notice:Content Author:「mikołak」,Reproduced under the CC 4.0 BY-SA copyright license with a link to the original source and this disclaimer.
Link to original article:https://stackoverflow.com/questions/71812481/minimal-generic-case-class-copy-function-in-scala-3

More about “Minimal, generic case class `copy` function in Scala 3” related questions

Minimal, generic case class `copy` function in Scala 3

In Scala (both 2 and 3), the copy method of case classes is synthetic, and so cannot be generalized using simple type clauses. In Scala 2, this led to generically processing case classes being done...

Show Detail

Scala case class, conditional Copy

I've defined a case class and a value: scala&gt; case class N(a:Int, b:Int) defined class N scala&gt; val nnn = N(2,3) nnn: N = N(2,3) I would like to modify a field based on an optional value, ...

Show Detail

Scala copy case class with generic type

I have two classes PixelObject, ImageRefObject and some more, but here are just these two classes to simplify things. They all are subclasses of a trait Object that contains an uid. I need universal

Show Detail

Scala: Copying a generic case class into another

I have the following setup, where I want to copy an instance of baseData into that of moreData: sealed trait baseData { def weight: Int def priority: Int } sealed trait moreData { def weig...

Show Detail

Defining a generic to be a case class

In this example, I want the generic T to be a case class and a DAOEntity with id, so in the abstract implementation, I can use the copy method. How to define it? trait DAOEntity { def id: String ...

Show Detail

Scala generic case class with optional field

I have the following generic case class to model a resources in an HTTP API (we use Akka HTTP): case class Job[Result]( id: String, result: Option[Result] = None, userId: String, ) and wa...

Show Detail

Is it possible to create a Codec Provider from a generic case class?

I am trying to make a generic function which produces a CodecProvider from a given generic case class. The BSON macro documentation does not give any examples of this. This (unanswered) SO quest...

Show Detail

Return copy of case class from generic function without runtime cast

I want to get rid of a runtime cast to a generic (asInstanceOf[A]) without implicit conversions. This happens when I have a fairly clean data model consisting of case classes with a common trait and

Show Detail

How to Decode a Generic Case Class with semiautomatic in Circe in Scala 3

The following code works with Scala 2.13 (see https://stackoverflow.com/a/59996748/2750966): import io.circe.generic.semiauto._ case class Name(name: String) case class QueryResult[T: Decoder](da...

Show Detail

Scala case class uses shallow copy or deep copy?

case class Person(var firstname: String, lastname: String) val p1 = Person("amit", "shah") val p2 = p1.copy() p1.firstname = "raghu" p1 p2 p1 == p2 As i went through some documentati

Show Detail